#import <Foundation/Foundation.h>
#import <SyncServices/SyncServices.h>
#import "notMac.h"

#include <stdio.h>
#include <stdlib.h>
#include <getopt.h>

static int verbose_flag;
	 
#define CONCAT_(a, b) a ## b
#define CONCAT(a, b) CONCAT_(a, b)
#define FOREACH(item, array) \
    NSEnumerator *CONCAT(__u_enum, __LINE__) = [array objectEnumerator]; \
        while ((item = [CONCAT(__u_enum, __LINE__) nextObject]))

NSString *UUID()
{
    CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
    NSString* str = (NSString*)CFUUIDCreateString(kCFAllocatorDefault, uuid);
    CFRelease(uuid);
    return [str autorelease];
}

int main(int argc, char **argv)
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    ISyncManager *manager = [ISyncManager sharedManager];
    ISyncClient *syncClient = nil;
    ISyncSession *syncSession = nil;

    NSString *clientIdentifier = @"net.notmac.sync";
    NSString *instanceIdentifier = nil;
    NSString *descriptionFilePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"sync.plist"];

    NSMutableArray *enable = nil;
    NSMutableArray *disable = nil;
    NSMutableArray *reset = nil;
    NSMutableArray *entityNames = nil;
    BOOL shouldRegister = NO;
    BOOL shouldUnregister = NO;

    static struct option long_options[] =
    {
	{"verbose", no_argument, &verbose_flag, 1},
	{"register", no_argument, 0, 'R' },
	{"unregister", no_argument, 0, 'U' },
	{"enable",  required_argument, 0, 'e' },
	{"disable", required_argument, 0, 'd' },
	{"reset", required_argument, 0, 'r' },
	{"sync", required_argument, 0, 's' },
	{"entitynames", required_argument, 0, 'n' },
	{0, 0, 0, 0}
    };

    int option_index = 0;
    int c;
	
    while ((c = getopt_long(argc, argv, "", long_options, &option_index)) >= 0)
    {
	switch (c)
	{
	case 0:
	    // setting flag
	    break;
	case 'R':
	    shouldRegister = YES;
	    break;
	case 'U':
	    shouldUnregister = YES;
	    break;
	case 'e':
	    if (!enable)
		enable = [[NSMutableArray alloc] init];
	    [enable addObjectsFromArray:[[NSString stringWithCString:optarg encoding:NSASCIIStringEncoding] componentsSeparatedByString:@","]];
	    break;
        case 'd':
	    if (!disable)
		disable = [[NSMutableArray alloc] init];
	    [disable addObjectsFromArray:[[NSString stringWithCString:optarg encoding:NSASCIIStringEncoding] componentsSeparatedByString:@","]];
	    break;
	case 'r':
	    if (!reset)
		reset = [[NSMutableArray alloc] init];
	    [reset addObjectsFromArray:[[NSString stringWithCString:optarg encoding:NSASCIIStringEncoding] componentsSeparatedByString:@","]];
	    break;
	case 's':
	    if (strcmp(optarg, [clientIdentifier cString]) != 0)
		errx(1, "unknown client identifier");
	    break;
	case 'n':
	    if (!entityNames)
		entityNames = [[NSMutableArray alloc] init];
	    [entityNames addObjectsFromArray:[[NSString stringWithCString:optarg encoding:NSASCIIStringEncoding] componentsSeparatedByString:@","]];
	    break;
	default:
	    NSLog(@"keycode=%d", c);
	    abort();
	}
    }

    NSString *entityName = nil;

    if (![manager isEnabled])
    {
	if (verbose_flag)
	    NSLog(@"manager is not enabled");

	return 0;
    }

    if (shouldRegister)
    {
	if (verbose_flag)
	    NSLog(@"registering");
		
	[manager registerClientWithIdentifier:clientIdentifier descriptionFilePath:descriptionFilePath];

	return 0;
    }

    if (!(syncClient = [manager clientWithIdentifier:clientIdentifier]))
    {
        if (verbose_flag)
	    NSLog(@"could not open client instance");

	return 1;
    }

    if (shouldUnregister)
    {
	if (verbose_flag)
	    NSLog(@"unregistering");
			
	[manager unregisterClient:syncClient];

	return 0;
    }
	
    if (enable || disable)
    {
	if (enable)
	    [syncClient setEnabled:YES forEntityNames:enable];

	if (disable)
	    [syncClient setEnabled:NO forEntityNames:disable];

        if (verbose_flag)
	{			
            FOREACH (entityName, [syncClient supportedEntityNames])
		NSLog(@"%c %@", [syncClient isEnabledForEntityName:entityName] ? '*' : ' ', entityName);
	}
		
	return 0;
    }

    if (reset)
    {
	if (verbose_flag)
	    NSLog(@"resetting");
			
	[syncSession clientDidResetEntityNames:reset];

	return 0;
    }

    if (!(instanceIdentifier = [syncClient objectForKey:@"identifier"]))
	    [syncClient setObject:instanceIdentifier = UUID() forKey: @"identifier"];

    notMac *account = [notMac notMacFromEnvironment];
    NSString *run;
	
    if (!(run = [account openSession:instanceIdentifier]))
    {
	if (verbose_flag)
	    NSLog(@"account validation failed");
			
	return 1;
    }

    if (!entityNames)
	entityNames = [[syncClient enabledEntityNames] copy];

    if (!(syncSession = [ISyncSession beginSessionWithClient:syncClient entityNames:entityNames beforeDate:[NSDate distantFuture]]))
    {
	if (verbose_flag)
	    NSLog(@"beginSessionWithClient for %@ failed", entityNames);
			
	return 1;
    }

    /* If we don't have any state for a particular entity name, we reset it and do a full sync */
    FOREACH(entityName, entityNames)
    {
        if ([syncClient objectForKey:entityName])
            continue;

        if (verbose_flag)
	    NSLog(@"reset state for %@", entityName);

        [syncSession clientDidResetEntityNames:[NSArray arrayWithObject:entityName]];
    }

    /* PUSH phase (i.e. notMac ---TO---> this app ---TO---> syncServer) */
    FOREACH(entityName, entityNames)
    {
	NSDictionary *state = [account getRecordsOfType:entityName sinceRun:[syncClient objectForKey:entityName]];
	NSDictionary *records = [state valueForKey:@"records"];
	NSArray *deleted = [state valueForKey:@"deleted"];
        NSString *identifier;
		
        FOREACH(identifier, [records allKeys])
        {
	    if (verbose_flag)
                NSLog(@"pushChangesFromRecord: %@", identifier);

            [syncSession pushChangesFromRecord:[NSUnarchiver unarchiveObjectWithData:[records valueForKey:identifier]] withIdentifier:identifier];
        }

        FOREACH(identifier, deleted)
        {
            if (verbose_flag)
		NSLog(@"deleteRecordWithIdentifier: %@", identifier);

            [syncSession deleteRecordWithIdentifier:identifier];
        }

	[syncClient setObject:run forKey:entityName];
    }
	
    /* PULL phase (i.e. syncServer ---TO---> this app ---TO---> notMac) */
    NSMutableArray *pull = [[NSMutableArray alloc] init];

    FOREACH(entityName, entityNames)
    {
	if ([syncSession shouldReplaceAllRecordsOnClientForEntityName:entityName])
        {
            if (verbose_flag)
		NSLog(@"shouldReplaceAllRecordsOnClientForEntityName: %@", entityName);

            [account deleteAllRecordsOfType:entityName];
        }

        if (([syncSession shouldPullChangesForEntityName:entityName]))
            [pull addObject:entityName];
    }
	
    [syncSession prepareToPullChangesForEntityNames:pull beforeDate:[NSDate distantFuture]];
	
    FOREACH(entityName, pull)
    {
        NSEnumerator *changeEnumerator = [syncSession changeEnumeratorForEntityNames:[NSArray arrayWithObject:entityName]];
        ISyncChange *currentChange;

        // Apply each change to the data store, and report success (only) to the sync server
        while ((currentChange = (ISyncChange *)[changeEnumerator nextObject]))
        {
	    switch ([currentChange type])
	    {
	    case ISyncChangeTypeDelete:
		if (verbose_flag)
		    NSLog(@"deleteRecordWithIdentifier: %@", [currentChange recordIdentifier]);

		if ([account deleteRecordWithIdentifier:[currentChange recordIdentifier] ofType:entityName])
		    [syncSession clientAcceptedChangesForRecordWithIdentifier:[currentChange recordIdentifier] formattedRecord:nil newRecordIdentifier:nil];
		else
		    NSLog(@"error deleting record: %@", [currentChange recordIdentifier]);

                break;
	    case ISyncChangeTypeAdd:
	    case ISyncChangeTypeModify:
		if (verbose_flag)
		    NSLog(@"replaceRecordwithIdentifier: %@", [currentChange recordIdentifier]);

		if ([account replaceRecord:[NSArchiver archivedDataWithRootObject:[currentChange record]] withIdentifier:[currentChange recordIdentifier] ofType:entityName])
		    [syncSession clientAcceptedChangesForRecordWithIdentifier:[currentChange recordIdentifier] formattedRecord:nil newRecordIdentifier:nil];
		else
		    NSLog(@"error replacing record: %@", [currentChange recordIdentifier]);

                break;
	    default:
                abort();
		break;
	    }
	}
    }

    [syncSession clientCommittedAcceptedChanges];

    [syncSession finishSyncing];
				
    [pool release];

    return 0;
}
